{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    " # Tutorial: DERController\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    " **This tutorial introduces the pandapower `DERController`**\n",
    "\n",
    " The `DERController` can be used to model Distributed Energy Resource (DER) capabilities for reactive power (Q) provision. The capabilities of reactive power provision may have differnent dependencies:\n",
    " + Q(P) capability: defines the reactive power capability depending on the DER active power generation\n",
    " + Q(V) capability: defines the reactive power capability depending on the voltage at the DER network connection point\n",
    "\n",
    " Different local control characteristics are included and can be applied via `DERController`, in particular:\n",
    " + const Q: fixed Q setpoint\n",
    " + fixed cosphi: fixed power factor\n",
    " + cosphi(P): power factor-watt function\n",
    " + Q(V): volt- var function\n",
    "\n",
    " Common DER capabilities and local control characteristics are predefined in DERController according to German Grid Codes."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    " **First of all we import all needed packages and libraries:**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-10-17T12:34:01.850676Z",
     "start_time": "2025-10-17T12:33:57.755904Z"
    }
   },
   "outputs": [],
   "source": [
    "import matplotlib.pyplot as plt\n",
    "from copy import deepcopy\n",
    "import pandas as pd\n",
    "import numpy as np\n",
    "\n",
    "from pandapower.run import runpp\n",
    "from pandapower.create import create_empty_network, create_buses, create_ext_grid, create_sgens, create_loads, create_lines, create_sgen, create_line\n",
    "from pandapower.control.controller.DERController import (\n",
    "    PQVArea4120V2,\n",
    "    QModelConstQ,\n",
    "    DERController,\n",
    "    QModelCosphiPCurve,\n",
    "    QModelCosphiP,\n",
    "    QModelQVCurve,\n",
    "    QVCurve,\n",
    "    PQVAreaPOLYGON,\n",
    "    PQAreaSTATCOM\n",
    ")\n",
    "from pandapower.control.controller.DERController.der_control_plot import plot_pq_area, plot_qv_area\n",
    "from pandapower.timeseries import OutputWriter, DFData, run_timeseries"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-10-17T12:34:01.865910Z",
     "start_time": "2025-10-17T12:34:01.856882Z"
    }
   },
   "outputs": [],
   "source": [
    "tex = False # this tutorial can use labels in latex style\n",
    "# since this requires a latex installation which is not included in the CI, it is set to False here\n",
    "if tex:\n",
    "    plt.rcParams['text.usetex'] = 'true'\n",
    "    plt.rcParams['text.latex.preamble'] = r'\\usepackage{amssymb} \\usepackage{amsmath} \\usepackage{xfrac}'\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    " Then, we create a simple **grid example** and with identically connected sgens, but with different active power feed-in. This enables to **check multiple active power injection states**."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-10-17T12:34:01.881386Z",
     "start_time": "2025-10-17T12:34:01.872387Z"
    }
   },
   "outputs": [],
   "source": [
    "p_vals_5 = np.linspace(0, 5, 26)\n",
    "\n",
    "def simple_test_net(p_vals):\n",
    "    \"\"\"Returns a net where n=len(p_vals) buses are connected to a external grid via a line of 5 km.\n",
    "    At each of those buses, an sgens and a load is connected. The sgens have the same installed \n",
    "    active power but linearly increasing momentary active power injection.\n",
    "    The demand of the loads is half of the momentary active power injections but distributed in the\n",
    "    opposite order.\n",
    "    \"\"\"\n",
    "    net = create_empty_network()\n",
    "    create_buses(net, 1+len(p_vals), vn_kv=20)\n",
    "    create_ext_grid(net, 0)\n",
    "    sgen_buses = list(range(1, len(p_vals)+1))\n",
    "    p_b_installed = max(p_vals)\n",
    "    create_sgens(net, sgen_buses, p_mw=p_vals, sn_mva=p_b_installed)\n",
    "    create_loads(net, sgen_buses, p_mw=p_vals[::-1]/2)\n",
    "    create_lines(net, [0]*len(p_vals), sgen_buses, length_km=5.0, std_type=\"NAYY 4x50 SE\")\n",
    "    return net\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    " **And finally, we define a function that is to plot the P/Q curves and Q/Vm curves from timeseries results:**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-10-17T12:34:01.896528Z",
     "start_time": "2025-10-17T12:34:01.888523Z"
    }
   },
   "outputs": [],
   "source": [
    "def plot_function(net, der_ctrl=None, pq_area=None, qv_area=None, ow=None, saturate_sn_mva=np.nan,\n",
    "                  show=True, tex=False):\n",
    "    label = 'Operation points'\n",
    "    # --- get pq_area and qv_area from inputs\n",
    "    if pq_area is None and der_ctrl is not None:\n",
    "        pq_area = getattr(getattr(der_ctrl, \"pqv_area\", {}), \"pq_area\", None)\n",
    "    if qv_area is None and der_ctrl is not None:\n",
    "        qv_area = getattr(getattr(der_ctrl, \"pqv_area\", {}), \"qv_area\", None)\n",
    "\n",
    "    # --- initiate figure and axes\n",
    "    n_cols = int(pq_area is not None) + int(qv_area is not None)\n",
    "    if n_cols == 0:\n",
    "        print(\"No plot made since no pq_area nor qv_area available.\")\n",
    "        return\n",
    "    fig1, axs = plt.subplots(ncols=n_cols, figsize=(5.25*n_cols+1.5, 4.5))\n",
    "    if n_cols == 2:\n",
    "        (ax1, ax2) = axs\n",
    "    elif pq_area is not None:\n",
    "        ax1 = axs\n",
    "    else:\n",
    "        ax2 = axs\n",
    "\n",
    "    # --- Plot P/Q diagram of allowed area and resulting points\n",
    "    if pq_area is not None:\n",
    "        S_N = net.sgen.sn_mva.at[0]\n",
    "        plot_pq_area(pq_area, title=pq_area.name(), ax=ax1, saturate_sn_pu=saturate_sn_mva/S_N,\n",
    "                       circle_segment=45, tex=tex)\n",
    "        xs = (net.res_sgen.q_mvar if ow is None else ow.output[\"res_sgen.q_mvar\"][0]) / S_N\n",
    "        ys = (net.res_sgen.p_mw if ow is None else ow.output[\"res_sgen.p_mw\"][0]) / S_N\n",
    "        for x, y in zip(xs, ys):\n",
    "            ax1.scatter(x, y, label=label, linestyle='-')\n",
    "        ax1.set_ylim(0, max(np.max(ys.values), 1)+0.1)\n",
    "\n",
    "    # --- Plot Q/V diagram of allowed area and resulting points\n",
    "    if qv_area is not None:\n",
    "        plot_qv_area(qv_area, title=qv_area.name(), ax=ax2, tex=tex)\n",
    "        xs = net.res_bus.vm_pu.iloc[1:] if ow is None else ow.output['res_bus.vm_pu'][1]\n",
    "        ys = (net.res_sgen.q_mvar if ow is None else ow.output[\"res_sgen.q_mvar\"][0]) / S_N\n",
    "        for x, y in zip(xs, ys):\n",
    "            ax2.scatter(x, y, label=label)\n",
    "\n",
    "    # --- if two parts plotted, remove legends of both parts and add a common legend\n",
    "    if n_cols == 2:\n",
    "        ax1.get_legend().remove()\n",
    "        ax2.get_legend().remove()\n",
    "        handles, labels = ax1.get_legend_handles_labels()\n",
    "        if tex:\n",
    "            labels = ['$Q_{min}$','$Q_{max}$','$S_{max}$',label]\n",
    "        else:\n",
    "            labels = ['Qmin','Qmax','Smax',label]\n",
    "        fig1.legend(handles, labels, ncol=4, framealpha=0, bbox_to_anchor=[0.7, 1.05])\n",
    "\n",
    "    plt.tight_layout()\n",
    "    if show:\n",
    "        plt.show()\n",
    "    return axs\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    " ## Q(P) Capability\n",
    " The Q(P) capability defines the reactive power capability of the DERs depending on their active power generation. The Q(P) capability of the DERs are usually defined in the national Requirements for Generators (RfG) or grid codes.\n",
    "\n",
    " In this simple example, a Q(P) capability according to the German medium voltage RfG VDE AR-N 4120 (Variant 2) is applied. We set a fixed reactive power setpoint `q=0.5` which is outside of the Q(P)-capability. Consequently, the `DERController` is needed to set the Q provision within its limits.\n",
    "\n",
    " The results of the following code, which gets plotted (first look at the plot left!), show that the reactive power is usually limited to `0.41 Sn` which is a power factor of `0.925 overexceited`. If the active power gets lower than `0.2 Sn`, also the reactive power gets reduced."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-10-17T12:34:04.661773Z",
     "start_time": "2025-10-17T12:34:01.903108Z"
    }
   },
   "outputs": [],
   "source": [
    "net = simple_test_net(p_vals_5)\n",
    "\n",
    "# define PQV area, Q model and DERController\n",
    "pqv_area = PQVArea4120V2()\n",
    "q_model = QModelConstQ(q_pu=0.5)\n",
    "ctrl_sgen_new = DERController(\n",
    "    net, net.sgen.index, q_model=q_model, pqv_area=pqv_area)\n",
    "\n",
    "# run power flows and plot\n",
    "runpp(net, run_control=True)\n",
    "axs = plot_function(net, ctrl_sgen_new, tex=tex)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    " ## Inverter apparent power limitation\n",
    "\n",
    "According to the most recent VDE AR N standards, reactive power provision must be available even for full active power injection.\n",
    "To, nevertheless, enable reducing power injection depending on maximum apparent power, such value can be passed as parameter `saturate_sn_mva` (default is NaN).\n",
    "If `saturate_sn_mva` is not NaN, `q_prio` defines which power is reduced to achieve the apparent power limitation. In other cases, the parameter is inactive.\n",
    "\n",
    "+ q_prio = True (default): DER active power is curtailed\n",
    "+ q_prio = False: DER reactive power is curtailed\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    " Let's repeat the last calculation with `saturate_sn_mva=1.02*net.sgen.p_mw.max()` and `q_prio=True` to see the difference to the last results (again focus on the left plot): Due to the active power reduction, there are no dots with `p > sqrt(saturate_sn_mva^2 - q^2)`.\n",
    " Also note that, according to `PQVArea4120V2`, the plotted green area stays limited to `p_mw <= sn_mva` (sn_mva is used as installed active power generation), independed from given `saturate_sn_mva` (dashed black line)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-10-17T12:34:05.674656Z",
     "start_time": "2025-10-17T12:34:04.668260Z"
    }
   },
   "outputs": [],
   "source": [
    "# change saturate_sn_mva using a new controller\n",
    "net = simple_test_net(p_vals_5)\n",
    "saturate_sn_mva_5 = 1.02 * net.sgen.p_mw.max()\n",
    "\n",
    "# define PQV area, Q model and DERController\n",
    "pqv_area = PQVArea4120V2()\n",
    "q_model = QModelConstQ(q_pu=0.5)\n",
    "\n",
    "ctrl_sgen_new = DERController(\n",
    "    net, net.sgen.index, q_model=q_model, pqv_area=pqv_area,\n",
    "    saturate_sn_mva=saturate_sn_mva_5, q_prio=True)\n",
    "\n",
    "# run timeseries and plot results\n",
    "runpp(net, run_control=True)\n",
    "axs = plot_function(net, ctrl_sgen_new, saturate_sn_mva=saturate_sn_mva_5, tex=tex)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    " If we consider `q_prio=False`, we can see in the left plot, that, for some operation points, q is reduced to zero to hold apparent power limits:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-10-17T12:34:06.675558Z",
     "start_time": "2025-10-17T12:34:05.685894Z"
    }
   },
   "outputs": [],
   "source": [
    "# change q_prio using a new controller\n",
    "net = simple_test_net(p_vals_5)\n",
    "\n",
    "# define PQV area, Q model and DERController\n",
    "ctrl_sgen_new = DERController(\n",
    "    net, net.sgen.index, q_model=QModelConstQ(q_pu=0.5),\n",
    "    pqv_area=PQVArea4120V2(), saturate_sn_mva=saturate_sn_mva_5, q_prio=False)\n",
    "\n",
    "# run timeseries and plot results\n",
    "runpp(net, run_control=True)\n",
    "axs = plot_function(net, ctrl_sgen_new, saturate_sn_mva=saturate_sn_mva_5, tex=tex)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    " ## Q(V) capability and voltage limitation function\n",
    "\n",
    " To visualize limiting the reactive power depending on the voltage of the RES connection point, we increase plant sizes up to 16 MW. Then there is sufficient voltage rise to show that the controller reacts on over voltages via decreasing the inductive reactive power provision (overexcited behavior). That reduction is not used if the active power is low since then no overvoltages occur."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-10-17T12:34:07.614603Z",
     "start_time": "2025-10-17T12:34:06.688837Z"
    }
   },
   "outputs": [],
   "source": [
    "p_vals_16 = np.linspace(0, 16, 26)\n",
    "net = simple_test_net(p_vals_16)\n",
    "\n",
    "# define PQV area, Q model and DERController\n",
    "ctrl_sgen_new = DERController(\n",
    "    net, net.sgen.index, q_model=QModelConstQ(q_pu=0.5),\n",
    "    pqv_area=PQVArea4120V2())\n",
    "\n",
    "# run power flows and plot\n",
    "runpp(net, run_control=True)\n",
    "axs = plot_function(net, ctrl_sgen_new, tex=tex)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    " To show that the PQ area, the QV area *and* the `saturate_sn_mva` parameter are considered simultaneously, we set a value to `saturate_sn_mva` that is below the highest apparent power reached:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-10-17T12:34:07.645689Z",
     "start_time": "2025-10-17T12:34:07.629268Z"
    }
   },
   "outputs": [],
   "source": [
    "s_sgen = pd.Series((net.res_sgen.p_mw**2 + net.res_sgen.q_mvar**2)**0.5, name=\"s_mva\")\n",
    "print(pd.concat([net.res_sgen, s_sgen], axis=1).iloc[-3:])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-10-17T12:34:07.988947Z",
     "start_time": "2025-10-17T12:34:07.653667Z"
    }
   },
   "outputs": [],
   "source": [
    "assert s_sgen.max() > 16.08  # -> saturate_sn_mva=16.08 MVA will be relevant\n",
    "\n",
    "net_saturated = deepcopy(net)\n",
    "net_saturated.controller.object.iat[0].saturate_sn_mva = 16.08\n",
    "runpp(net_saturated, run_control=True)\n",
    "\n",
    "s_sgen_saturated = pd.Series((net_saturated.res_sgen.p_mw**2 + net_saturated.res_sgen.q_mvar**2)**0.5, name=\"s_mva\")\n",
    "assert np.isclose(s_sgen_saturated.max(), 16.08, atol=1e-5)\n",
    "print(pd.concat([net_saturated.res_sgen, s_sgen_saturated], axis=1).iloc[-3:])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    " ## Modeling reactive power characteristics\n",
    " Among a static reactive power provision, as previously used in the tutorial (`q=0.5`), there are more advanced and grid-supporting reactive power behaviors.\n",
    "\n",
    " ### Cosphi(P) curve\n",
    " Because active power feed-in leads to a voltage increase, an `cosphi(P)` characteristic is applied by some distributed energy resources (DERs) to counteract the voltage rise by underexcited reactive power behavior, as exemplified in the following."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-10-17T12:34:08.955332Z",
     "start_time": "2025-10-17T12:34:07.996843Z"
    }
   },
   "outputs": [],
   "source": [
    "net_cosphiP = simple_test_net(p_vals_16)\n",
    "\n",
    "# define PQV area, Q model and DERController\n",
    "q_model=QModelCosphiPCurve(cosphi_p_curve={\n",
    "        'p_points_pu':(0, 0.5, 1), 'cosphi_points':(1, 1, -0.95)})\n",
    "\n",
    "ctrl_sgen_new = DERController(\n",
    "    net_cosphiP, net_cosphiP.sgen.index, q_model=q_model, pqv_area=PQVArea4120V2())\n",
    "\n",
    "# run power flows and plot\n",
    "runpp(net_cosphiP, run_control=True)\n",
    "axs = plot_function(net_cosphiP, ctrl_sgen_new, tex=tex)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "In constrast to the previous fixed and overexcited reactive power provision, `cosphi(P)` reduces the voltage rise and prevents to reach the upper limit of the QV area in this example. The following code provides the resulting numbers to compare."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-10-17T12:34:08.986981Z",
     "start_time": "2025-10-17T12:34:08.960312Z"
    }
   },
   "outputs": [],
   "source": [
    "greater_0_5 = net.sgen.index[net.sgen.p_mw > 0.5]\n",
    "df_compare = pd.DataFrame({\n",
    "    \"Vm_qfix\": net.res_bus.vm_pu.loc[net.sgen.bus.loc[greater_0_5]].values,\n",
    "    \"Vm_cosphiP\": net_cosphiP.res_bus.vm_pu.loc[net_cosphiP.sgen.bus.loc[greater_0_5]].values,\n",
    "    \"P_qfix\": net.res_sgen.p_mw.loc[greater_0_5],\n",
    "    \"P_cosphiP\": net_cosphiP.res_sgen.p_mw.loc[greater_0_5],\n",
    "    \"Q_qfix\": net.res_sgen.q_mvar.loc[greater_0_5],\n",
    "    \"Q_cosphiP\": net_cosphiP.res_sgen.q_mvar.loc[greater_0_5],\n",
    "})\n",
    "print(df_compare)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    " ### Cosphi(P)\n",
    "\n",
    " In addition, a model for a fixed cosphi depending on the momentary active power is available."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-10-17T12:34:10.045343Z",
     "start_time": "2025-10-17T12:34:08.992972Z"
    }
   },
   "outputs": [],
   "source": [
    "net = simple_test_net(p_vals_16)\n",
    "\n",
    "# define PQV area, Q model and DERController\n",
    "q_model = QModelCosphiP(cosphi=-0.95)\n",
    "\n",
    "ctrl_sgen_new = DERController(\n",
    "    net, net.sgen.index, q_model=q_model, pqv_area=PQVArea4120V2())\n",
    "\n",
    "# run power flows and plot\n",
    "runpp(net, run_control=True)\n",
    "axs = plot_function(net, ctrl_sgen_new, tex=tex)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    " ### Q(V) curve\n",
    " A widespread reactive power behavior in recent DERs is applying a Q(V) characteristic. The curve of the Q(V) characteristic can be freely customizable. The exemplary curve, that is applied in the following example, is plotted in the QV area by a grey line. The reactive power reduction in cases of very low active power injections causes deviations to that custom Q(V) curve."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-10-17T12:34:10.953631Z",
     "start_time": "2025-10-17T12:34:10.053343Z"
    }
   },
   "outputs": [],
   "source": [
    "net = simple_test_net(p_vals_16)\n",
    "\n",
    "# define PQV area, Q model and DERController\n",
    "q_model = QModelQVCurve(qv_curve=QVCurve(\n",
    "    vm_points_pu=(0, 0.90, 1.00, 1.02, 2),\n",
    "    q_points_pu=(0.312, 0.312, 0, -0.312, -0.312)))\n",
    "\n",
    "ctrl_sgen_new = DERController(\n",
    "    net, net.sgen.index, q_model=q_model, pqv_area=PQVArea4120V2())\n",
    "\n",
    "# run power flows and plot\n",
    "runpp(net, run_control=True)\n",
    "axs = plot_function(net, ctrl_sgen_new, show=False, tex=tex)\n",
    "\n",
    "# plot theoretical QVCurve\n",
    "vm_curve = np.linspace(0.85, 1.20, 200)\n",
    "q_curve = np.interp(vm_curve, q_model.qv_curve.vm_points_pu, q_model.qv_curve.q_points_pu)\n",
    "axs[1].plot(vm_curve, q_curve, color=\"grey\")\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## PQ and QV Areas\n",
    "So far, 'PQVArea4120V2' has been used as an example. Such PQV areas are available for all voltage levels\n",
    "* EHV (380 kV and 220 kV): PQVArea4130\n",
    "* HV (110 kV): PQVArea4120\n",
    "* MV (e.g. 10 kV or 20 kV): PQVArea4110\n",
    "* LV (e.g. 0.4 kV): PQVArea4105\n",
    "and also in the different variants if standardized.\n",
    "\n",
    "### Polygonal PQV area\n",
    "Any piecewise linear areas can be defined and used with the help of `PQVAreaPOLYGON`. The following code box provides an example."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-10-17T12:34:11.908196Z",
     "start_time": "2025-10-17T12:34:10.960609Z"
    }
   },
   "outputs": [],
   "source": [
    "net = simple_test_net(p_vals_16)\n",
    "\n",
    "# define PQV area, Q model and DERController\n",
    "q_model=QModelQVCurve({\n",
    "        \"vm_points_pu\": (0, 0.98, 1.06),\n",
    "        \"q_points_pu\": (0.484, 0.484, -0.484)})\n",
    "\n",
    "p_points_pu = (0.1, 0.2, 1, 1, 0.2, 0.1, 0.1)\n",
    "q_pq_points_pu = (0.1, 0.410775, 0.410775, -0.328684, -0.328684, -0.1, 0.1)\n",
    "q_qv_points_pu = (0.410775, 0.410775, -0.328684, -0.328684, 0.410775)\n",
    "vm_points_pu = (1.1, 0.9, 0.9, 1.1, 1.1)\n",
    "pqv_area = PQVAreaPOLYGON(p_points_pu, q_pq_points_pu, q_qv_points_pu, vm_points_pu)\n",
    "\n",
    "ctrl_sgen_new = DERController(\n",
    "    net, net.sgen.index, q_model=q_model, pqv_area=pqv_area)\n",
    "    \n",
    "# run power flows and plot\n",
    "runpp(net, run_control=True)\n",
    "axs = plot_function(net, ctrl_sgen_new, tex=tex)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Multiple active power injection states can also be analyzed performing a timeseries calculation of one controller sgen. This is applied in the following for demonstration reasons. Furthermore, negative active power values are assumed in the timeseries data to show the functionality of profile_sacle."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-10-17T12:34:11.924259Z",
     "start_time": "2025-10-17T12:34:11.916859Z"
    }
   },
   "outputs": [],
   "source": [
    "def simple_test_net_for_timeseries():\n",
    "    # this is basically the same grid as simple_test_net() but with only one line to one bus\n",
    "    # with one sgen\n",
    "    net = create_empty_network()\n",
    "    create_buses(net, 2, vn_kv=20)\n",
    "    create_ext_grid(net, 0)\n",
    "    create_sgen(net, 1, p_mw=5., sn_mva=5.)\n",
    "    create_line(net, 0, 1, length_km=5.0, std_type=\"NAYY 4x50 SE\")\n",
    "\n",
    "    # create outputwriter and define variables to log\n",
    "    ow = OutputWriter(net)\n",
    "    ow.log_variable(\"res_sgen\", \"p_mw\")\n",
    "    ow.log_variable(\"res_sgen\", \"q_mvar\")\n",
    "\n",
    "    return net, ow\n",
    "\n",
    "ts_data = pd.DataFrame({\"p_0\": -p_vals_5})\n",
    "data = DFData(ts_data)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Statcom capability\n",
    "Statcom capability does not depend on active power p as the previous where the flexibility is reduced in low injection cases.\n",
    "`min_q` and `max_q` are reactive power limits that are adjustable and user defined parameters.\n",
    "A limitation corresponding to `saturate_sn_mva` is possible with this PQ area, as shown above.\n",
    "\n",
    "If only one of both flexibility areas, PQ area or QV area, should be considered, this area can be passed as `pqv_area` directly to the DERController.\n",
    "This is illustrated in the following example, as well."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-10-17T12:34:13.655688Z",
     "start_time": "2025-10-17T12:34:11.930426Z"
    }
   },
   "outputs": [],
   "source": [
    "net, ow = simple_test_net_for_timeseries()\n",
    "\n",
    "# define PQV area, Q model and DERController\n",
    "pq_area = PQAreaSTATCOM(min_q_pu=-0.33, max_q_pu=0.33)\n",
    "\n",
    "ctrl_sgen_new = DERController(\n",
    "    net, 0, q_model=QModelConstQ(q_pu=0.5),\n",
    "    pqv_area=pq_area,\n",
    "    data_source=data, p_profile=\"p_0\", profile_scale=-1)\n",
    "\n",
    "# run power flows and plot\n",
    "run_timeseries(net, time_steps=range(len(p_vals_5)))\n",
    "axs = plot_function(net, pq_area=pq_area, ow=ow, tex=tex)\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3.10.11 ('base')",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3"
  },
  "vscode": {
   "interpreter": {
    "hash": "19d1d53a962d236aa061289c2ac16dc8e6d9648c89fe79f459ae9a3493bc67b4"
   }
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
